package org.mysterylab.utopiaframework.commons.compass.service;

import org.compass.core.Compass;
import org.compass.core.CompassCallback;
import org.compass.core.CompassDetachedHits;
import org.compass.core.CompassException;
import org.compass.core.CompassHits;
import org.compass.core.CompassQuery;
import org.compass.core.CompassSession;
import org.compass.core.CompassTemplate;
import org.compass.core.CompassTransaction;
import org.compass.core.support.search.CompassSearchCommand;
import org.compass.core.support.search.CompassSearchResults;
import org.compass.spring.web.mvc.AbstractCompassCommandController;
import org.compass.spring.web.mvc.CompassSearchController;
import org.mysterylab.utopiaframework.commons.compass.command.AdvancedSearchCommand;
import org.mysterylab.utopiaframework.commons.compass.command.CompassSort;
import org.springframework.beans.factory.InitializingBean;

/**
 * 仿照 {@link CompassSearchController} 中的代码, 构建一个创建索引的 Service,
 * 这样在某些非 Spring MVC 的场合(比如 Struts) 中可以让 Action 中调用该 Service
 * 来完成查找的任务. 比如在 SearchAction.java 中可以这样写(SSH 架构):
 * <pre>
 *  public class SearchAction extends org.apache.struts.actions.DispatchAction implements InitializingBean {
 *      private Integer pageSize;
 *      private Compass compass;
 *      private CompassTemplate compassTemplate;
 *      
 *      public CompasSearchResults search(final CompassSearchCommand command) {...}
 *      
 *      protected CompassSearchResults performSearch(CompassSearchCommand searchCommand, CompassSession session) {...}
 *      
 *      protected CompassQuery buildQuery(CompassSearchCommand searchCommand, CompassSession session) {...}
 *      
 *      protected void doProcessBeforeDetach(CompassSearchCommand searchCommand, CompassSession session, CompassHits hits, int from, int size) {}
 *      
 *      protected void doProcessAfterDetach(CompassSearchCommand searchCommand, CompassSession session, CompassDetachedHits hits) {}
 *      
 *      public void afterPropertiesSet() throws Exception {...}
 *      
 *      public ActionForward execute(ActionMapping mapping, ActionForm form, HttpServletRequest request, HttpServletResponse response) throws Exception {
 *          SearchForm searchForm = (SearchForm) form;
 *          String query = searchForm.getQuery();
 *          CompassSearchCommand command;
 *          if (StringUtils.isEmpty(query)) {
 *              return mapping.findForward(Constants.FAILURE_KEY);
 *          }
 *          command = new CompassSearchCommand(query);
 *          CompassSearchResults searchResults = search(command);
 *          request.setAttribute("command", command);
 *          request.setAttribute("searchResults", searchResults);
 *          return mapping.findForward(Constants.SUCCESS_KEY);
 *      }
 *  }
 * </pre>
 * 
 * @author zhouych
 * @see AbstractCompassCommandController
 * @see CompassSearchController
 */
public class CompassSearchService implements InitializingBean {
	
	/**
	 * 每页显示的条目数量
	 */
	private Integer pageSize;
	
	/**
     * 用来创建线程安全的 {@link CompassSession}, 另外提供一些搜索引擎索引级别的操作
     */
	private Compass compass;
	
	/**
	 * 抽象事务管理
	 */
	private CompassTemplate compassTemplate;

	/**
	 * 公开的搜索接口, 返回匹配的搜索结果.
	 * 
	 * @param command
	 * @return
	 */
	public CompassSearchResults search(final CompassSearchCommand command) {
		CompassSearchResults searchResults = (CompassSearchResults) getCompassTemplate()
				.execute(
						CompassTransaction.TransactionIsolation.READ_ONLY_READ_COMMITTED,
						new CompassCallback<Object>() {
							@Override
							public Object doInCompass(CompassSession session)
									throws CompassException {
								return performSearch(command, session);
							}
						});
		return searchResults;
	}

	/**
	 * 通过此方法调用搜索引擎, 进行结果匹配搜索.
	 * 
	 * @param searchCommand
	 * @param session
	 * @return
	 */
	protected CompassSearchResults performSearch(CompassSearchCommand searchCommand,
			CompassSession session) {
		
		// 其实这里 AdvanceSearchCommand 肯定是 CompassSearchCommand 的继承类
		if (AdvancedSearchCommand.class.isAssignableFrom(searchCommand.getClass())) {
			AdvancedSearchCommand advancedCommand = (AdvancedSearchCommand) searchCommand;
			advancedCommand.setupQuery();
		}
		long time = System.currentTimeMillis();
		CompassQuery query = buildQuery(searchCommand, session);
		CompassHits hits = query.hits();
		CompassDetachedHits detachedHits;
		CompassSearchResults.Page[] pages = null;
		if (pageSize == null) {
			doProcessBeforeDetach(searchCommand, session, hits, -1, -1);
			detachedHits = hits.detach();
		} else {
			int iPageSize = pageSize.intValue();
			int page = 0;
			int hitsLength = hits.getLength();
			if (searchCommand.getPage() != null) {
				page = searchCommand.getPage().intValue();
			}
			int from = page * iPageSize;

			if (from > hits.getLength()) {
				
				// 如果起始的条目大于搜索到的条目
				from = hits.getLength() - iPageSize;
				doProcessBeforeDetach(searchCommand, session, hits, from, hitsLength);
				detachedHits = hits.detach(from, hitsLength);
			} else if ((from + iPageSize) > hitsLength) {
				
				// 结束的条目大于搜索到的结果
				doProcessBeforeDetach(searchCommand, session, hits, from, hitsLength);
				detachedHits = hits.detach(from, hitsLength);
			} else {
				
				// 中间的页码，直接取出相应的条目
				doProcessBeforeDetach(searchCommand, session, hits, from, iPageSize);
				detachedHits = hits.detach(from, iPageSize);
			}
			doProcessAfterDetach(searchCommand, session, detachedHits);
			int numberOfPages = (int) Math.ceil((float) hitsLength / iPageSize);
			pages = new CompassSearchResults.Page[numberOfPages];
			for (int i = 0; i < pages.length; i++) {
				pages[i] = new CompassSearchResults.Page();
				pages[i].setFrom(i * iPageSize + 1);
				pages[i].setSize(iPageSize);
				pages[i].setTo((i + 1) * iPageSize);
				if (from >= (pages[i].getFrom() - 1) && from < pages[i].getTo()) {
					pages[i].setSelected(true);
				} else {
					pages[i].setSelected(false);
				}
			}
			if (numberOfPages > 0) {
				CompassSearchResults.Page lastPage = pages[numberOfPages - 1];
				if (lastPage.getTo() > hitsLength) {
					lastPage.setSize(hitsLength - lastPage.getFrom());
					lastPage.setTo(hitsLength);
				}
			}
		}
		
		time = System.currentTimeMillis() - time;
		CompassSearchResults searchResults = new CompassSearchResults(
				detachedHits.getHits(), time, pageSize);
		searchResults.setPages(pages);
		return searchResults;
	}

	/**
	 * 构建 Lucene 搜索器.
	 * 
	 * @param searchCommand
	 * @param session
	 * @return
	 */
	protected CompassQuery buildQuery(CompassSearchCommand searchCommand,
			CompassSession session) {
		CompassQuery query = session.queryBuilder().queryString(
				searchCommand.getQuery().trim()).toQuery();

		if (AdvancedSearchCommand.class.isAssignableFrom(searchCommand
				.getClass())) {
			AdvancedSearchCommand advancedSearchCommand = (AdvancedSearchCommand) searchCommand;

			for (CompassSort sort : advancedSearchCommand.getSortMap()) {
				query.addSort(sort.getName(), sort.getType(), sort.getDirection());
			}
		}
		return query;
	}

	/**
	 * 在 detach 之前, 可以做一些操作. 比如 highlighting 等.
	 * 
	 * @param searchCommand
	 * @param session
	 * @param hits
	 * @param from
	 *            需要注意的是，如果 pageSize 没有指定，那么这里传入的参数为 -1
	 * @param size
	 */
	protected void doProcessBeforeDetach(CompassSearchCommand searchCommand,
			CompassSession session, CompassHits hits, int from, int size) {
	}

	protected void doProcessAfterDetach(CompassSearchCommand searchCommand,
			CompassSession session, CompassDetachedHits hits) {

	}

	@Override
	public void afterPropertiesSet() throws Exception {
		if (compass == null) {
			throw new IllegalArgumentException("Must set compass proerty");
		}
		this.compassTemplate = new CompassTemplate(compass);
	}

	public Integer getPageSize() {
		return pageSize;
	}

	public void setPageSize(Integer pageSize) {
		this.pageSize = pageSize;
	}

	public void setCompass(Compass compass) {
		this.compass = compass;
	}

	protected CompassTemplate getCompassTemplate() {
		return this.compassTemplate;
	}

}
